﻿//#define _CRT_SECURE_NO_WARNINGS
//#include <stdio.h>
//#include <stdlib.h>
//#include <errno.h>
//#include <string.h>
//#include <windows.h>
// C/C++ 学习网站：https://legacy.cplusplus.com 注意旧版本（搜索）和新版本

#include "common.h"

/*
VS 2022 可以设置高级保存选项（拓展-自定义菜单-命令中添加文件高级保存选项）来调整文件编码保存设置
任何一个 C 程序，运行时都自动打开三个流：
    FILE* stdin 标准输入流，对应键盘输入
    FILE* stdout 标准输出流，对应屏幕输出
    FILE* stderr 标准错误流，对应屏幕输出错误信息

文件名：
    文件名包含三个部分：
        1. 路径名：文件所在的目录
        2. 文件名：文件名本身
        3. 扩展名：文件名的后缀名，如.txt,.c,.cpp等

文件指针：
    缓冲文件系统中, 关键的概念是 “文件类型指针”, 简称“文件指针”。
    每个被使用的文件都在内存中开辟了一个相应的文件信息区, 用来存放文件的相关信息(如文件的名字, 文件状态
    及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是有系统声明的, 取名 FILE
    每当打开一个文件的时候, 系统会根据文件的情况自动创建一个FILE结构的变量, 并填充其中的信息,
    使用者不必关心细节。一般都是通过一个FILE的指针来维护这个FILE结构的变量,这样使用起来更加方便。

例如,VS2013 编译环境提供的 stdio.h 头文件中有以下的文件类型申明:
    struct _iobuf {
        char *_ptr;
        int _cnt;
        char *_base;
        int _flag;
        int _file;
        int _charbuf;
        int _bufsiz;
        char *_tmpfname;
    };
    typedef struct _iobuf FILE;
    不同的C编译器的FILE类型包含的内容不完全相同,但是大同小异。

下面我们可以创建一个 FILE* 的指针变量:
    FILE* pf; // 文件指针变量
    定义 pf 是一个指向FILE类型数据的指针变量。可以使 pf 指向某个文件的文件信息区(是一个结构体变量)。
    通过该文件信息区中的信息就能够访问该文件。也就是说, 通过文件指针变量能够找到与它关联的文件。

文件操作函数：
    1. FILE* fopen(const char *filename, const char *mode)
        打开在参数 filename 中指定其名称的文件，并将其与在将来的操作中可以通过返回的 FILE
        指针标识的流相关联。mode 参数:
            "r" 以只读方式打开文件。该文件必须存在。
            "w" 为输出操作创建一个空文件。如果已存在同名文件，则其内容将被丢弃，并将该文件视为新的空文件。
            "a" 打开文件以在文件末尾输出。输出操作始终在文件末尾写入数据，并对其进行扩展。
                重新定位操作将被忽略。如果文件不存在，则创建该文件。
            "r+" 开放一个文件进行更新（用于输入和输出）。该文件必须存在。
            "w+" 创建一个空文件并打开它以进行更新（用于输入和输出）。如果已存在同名文件，
                则其内容将被丢弃，并将该文件视为新的空文件。
            "a+" 打开一个要更新的文件（用于输入和输出），所有输出操作都在文件末尾写入数据。
                重新定位操作会影响下一个输入操作，但输出操作会将位置移回文件末尾。
                如果文件不存在，则创建该文件。
            “rb"(只读) 为了输入数据, 打开一个二进制文件，文件必须存在。
            “wb"(只写) 为了输出数据, 创建一个新的二进制文件，如果文件已存在, 则其内容将被丢弃。
            “ab”(追加) 打开一个二进制文件以在文件末尾输出。输出操作始终在文件末尾写入数据，
                并对其进行扩展。文件必须存在。
            “rb+”(读写) 打开一个二进制文件进行更新（用于输入和输出）。该文件必须存在。
            “wb+”(读写) 创建一个新的二进制文件并打开它以进行更新（用于输入和输出）。
                如果已存在同名文件，则其内容将被丢弃，并将该文件视为新的空文件。
            “ab+”(读写) 打开一个要更新的二进制文件（用于输入和输出），所有输出操作都在文件末尾写入数据。
                重新定位操作会影响下一个输入操作，但输出操作会将位置移回文件末尾。文件必须存在。
        成功打开文件后，函数返回一个指向 FILE 结构的指针，该指针可用于后续文件操作。
        如果文件无法打开，则返回 NULL。
    2. int fclose(FILE *fp)
           关闭与 fp 关联的文件。成功关闭后，函数返回 0。如果文件无法关闭，则返回 EOF。
    3. int fputc(int c, FILE* fp)
           将 c 写入到 fp 指向的文件中。成功写入后，函数返回 c。如果写入失败，则返回 EOF。
    4. int fgetc(FILE* fp)
           从 fp 指向的文件中读取一个字符。成功读取后，函数返回读取的字符。如果读取失败，则返回 EOF。
    5. int fputs(const char *s, FILE *fp)
           将字符串 s 写入到 fp 指向的文件中。成功写入后，函数返回 0。如果写入失败，则返回 EOF。
    6. char* fgets(char *s, int n, FILE *fp)
           从 fp 指向的文件中读取 n-1 个字符，第 n 个字符为换行符，并将它们存储在字符串 s 中。
           成功读取后，函数返回 s。如果读取失败，则返回 NULL。n 之前遇到换行符，则停止读取。
           如果文件已到达末尾，则返回 EOF。
    7. int fprintf(FILE *fp, const char *format,...)
           格式化结构数据输出到 fp 指向的文件中。成功写入后，函数返回写入的字符数。
           如果写入失败，则返回 EOF。格式化字符串 format 与 printf() 函数中的格式字符串相同。
    8. int fscanf(FILE *fp, const char *format,...)
           从 fp 指向的文件中读取格式化输入。成功读取后，函数返回读取的字符数。如果读取失败，则返回 EOF。
           格式化字符串 format 与 scanf() 函数中的格式字符串相同。
    9. size_t fread(void *ptr, size_t size, size_t nmemb, FILE *fp)
           从 fp 指向的二进制文件中读取 nmemb 个 size 字节的块，并将它们存储在 ptr 指向的缓冲区中。
           成功读取后，函数返回实际读取的块数目。如果读取失败，则返回 0。只针对文件流
    10. size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *fp)
            将 ptr 指向的缓冲区中 nmemb 个 size 字节的块写入到 fp 指向的文件中。成功写入后，
            函数返回实际写入的块数目。如果写入失败，则返回 0。只针对文件流
    11. int fseek(FILE *fp, long int offset, int whence)
            whence 参数:
                SEEK_SET 相对于文件开头。
                SEEK_CUR 相对于当前位置。
                SEEK_END 相对于文件末尾。
            并调整 fp 指向的文件的当前位置(offset)。
            函数返回 0 表示成功，非 0 表示失败。
            对于以文本模式打开的流，offset 应为零或上一次调用 ftell 返回的值，
            并且 whence 必须为 SEEK_SET。
    12. long ftell(FILE *fp)
            返回 fp 指向的文件的当前位置。函数返回当前位置的偏移量。如果出错，则返回 -1L。
    13. int feof(FILE *fp)
            如果 fp 指向的文件的当前位置已经到达文件末尾，则返回非 0 值，否则返回 0；可以自定义。
            注意：一般不用这个函数判断文件是否已经结束。
            文本文件读取是否结束, 判断返回值是否为 EOF(fgetc), 或者 NULL(fgets)
                fgetc 判断是否为 EOF
                fgets 判断返回值是否为 NULL
            二进制文件的读取结束判断, 判断返回值是否小于实际要读的个数。
                fread 判断返回值是否小于实际要读的个数
    14. int ferror(FILE *fp)
            如果在上一次文件操作中发生错误，则返回非 0 值。否则返回 0。
    15. int remove(const char *filename)
            删除文件 filename。成功删除后，函数返回 0。如果文件不存在或无法删除，则返回 -1。
    16. int rename(const char *oldname, const char *newname)
            重命名文件 oldname 为 newname。成功重命名后，函数返回 0。
            如果文件不存在或无法重命名，则返回 -1。
    17. int fflush(FILE *fp)
            强制将缓冲区中的数据写入到 fp 指向的文件中。成功写入后，函数返回 0。
            如果写入失败，则返回 EOF。
    18. int setvbuf(FILE *fp, char *buf, int mode, size_t size)
            设置 fp 指向的文件的缓冲模式。mode 参数:
                _IONBF 无缓冲模式。
                _IOFBF 全缓冲模式。
                _IOLBF 行缓冲模式。
            size 参数指定缓冲区的大小。函数返回 0 表示成功，非 0 表示失败。
    19. int fputc_unlocked(int c, FILE* fp)
            与 fputc() 函数相同，但不受线程锁定影响。
    20. int fgetc_unlocked(FILE* fp)
            与 fgetc() 函数相同，但不受线程锁定影响。
    21. int fputs_unlocked(const char *s, FILE *fp)
            与 fputs() 函数相同，但不受线程锁定影响。
    22. char* fgets_unlocked(char *s, int n, FILE *fp)
            与 fgets() 函数相同，但不受线程锁定影响。
    23. int fprintf_unlocked(FILE *fp, const char *format,...)
            与 fprintf() 函数相同，但不受线程锁定影响。
    24. int fscanf_unlocked(FILE *fp, const char *format,...)
            与 fscanf() 函数相同，但不受线程锁定影响。
    25. int fseek_unlocked(FILE *fp, long offset, int whence)
            与 fseek() 函数相同，但不受线程锁定影响。
    26. long ftell_unlocked(FILE *fp)
            与 ftell() 函数相同，但不受线程锁定影响。
    27. int feof_unlocked(FILE *fp)
            与 feof() 函数相同，但不受线程锁定影响。
    28. int ferror_unlocked(FILE *fp)
            与 ferror() 函数相同，但不受线程锁定影响。
    29. int remove_unlocked(const char *filename)
            与 remove() 函数相同，但不受线程锁定影响。
    30. int rename_unlocked(const char *oldname, const char *newname)
            与 rename() 函数相同，但不受线程锁定影响。
    31. int fflush_unlocked(FILE *fp)
            与 fflush() 函数相同，但不受线程锁定影响。
    32. int setvbuf_unlocked(FILE *fp, char *buf, int mode, size_t size)
            与 setvbuf() 函数相同，但不受线程锁定影响。

文件与二进制文件：
    二进制文件是指文件中存储的数据是以二进制形式存储的，而不是以文本形式存储的。
    二进制文件可以用于存储各种各样的数据，如图像、声音、视频等。
    文本文件通常是 ASCII 编码形式存储的.So文本文件和二进制文件储存的大小可能不同。
        例如：10000 个字符的文本文件，其大小为 5 个字符（5 个 ASCII 码）
        而 10000 个字符的二进制文件，其大小为 4 个字节（可能是一个 int 型数据）
    VS 编译器会把二进制文件以小端方式存储在磁盘上。

缓冲区：
    缓冲区是指在内存中开辟一块空间，用来暂时存放文件中的数据。
    缓冲区的大小决定了文件操作的效率。
    缓冲区的大小可以通过 setvbuf() 函数设置。
    缓冲区的作用：
        1. 减少磁盘 I/O 次数，提高文件操作效率。
        2. 缓冲区可以提高文件操作的稳定性。
        3. 缓冲区可以实现文件随机访问。
缓冲区的大小：缓冲区的大小决定了一次性从磁盘读取或写入的字节数，
    缓冲区越大，一次性读写的字节数越多，效率越高，但也会占用更多内存。
缓冲区的位置：缓冲区的位置决定了文件指针的位置，
    缓冲区越靠前，文件指针越靠前，每次读写都需要先从缓冲区中读取或写入数据，效率高；
    缓冲区越靠后，文件指针越靠后，每次读写都需要先将数据写入缓冲区，效率低。
缓冲区的模式：缓冲区的模式决定了缓冲区的读写方式，
缓冲区的模式有三种：全缓冲、行缓冲、无缓冲。
    全缓冲模式：缓冲区中存放整个文件，每次读写都需要从缓冲区中读取或写入数据，效率高。
    行缓冲模式：缓冲区中存放一行数据，每次读写都需要从缓冲区中读取或写入一行数据，效率次高。
    无缓冲模式：不使用缓冲区，每次读写都需要直接从磁盘中读取或写入数据，效率低。
缓冲区的大小和模式：
    对于文本文件，缓冲区的大小一般为 4KB，模式为行缓冲；
    对于二进制文件，缓冲区的大小一般为 8KB，模式为全缓冲。
缓冲区的位置：
    对于文本文件，缓冲区的位置一般在文件开始位置；
    对于二进制文件，缓冲区的位置一般在文件结束位置。
缓冲区的作用：
    对于文本文件，缓冲区的作用是减少磁盘 I/O 操作，提高文件读写效率；
    对对于二进制文件，缓冲区的作用是减少内存的使用，提高文件读写效率。
缓冲区的选择：
    对于文本文件，选择全缓冲模式或行缓冲模式；
    对于二进制文件，选择全缓冲模式。
缓冲区的使用：
    对于文本文件，使用 fgets() 函数，一次读取一行数据；
    对于二进制文件，使用 fread() 函数，一次读取一个结构体或其他数据。
缓冲区的刷新：
    对于文本文件，使用 fflush() 函数，刷新缓冲区；
    对于二进制文件，使用 fflush() 函数，刷新缓冲区。
缓冲区的关闭：
    对于文本文件，使用 fclose() 函数，关闭文件；
    对于二进制文件，使用 fclose() 函数，关闭文件。

*/

typedef struct Student {
    char name[20];
    int age;
    float score;
} student;


// 测试文件读写等操作
void file_test()
{
    FILE* fp;
    // 读文件部分
    {
        print_start_line("读文件部分测试开始");
        // 设置文件编码格式
        //SetConsoleOutputCP(65001); // 65001表示UTF-8编码
        fp = fopen("fopen_test.txt", "w");
        if (fp == NULL) {
            printf("文件打开失败！%s\n", strerror(errno));
            return errno;
        }
        //在 C 语言中，文件的编码格式通常是与写入的内容一致的，而不是直接通过某种 API 设置的。也就是说，如果你写入的字符串本身就是 UTF-8 编码的，那么写入的文件也将是 UTF-8 编码的。
        char str[] = u8"this is first row\n"
            "abcdefghijklmnopqrstuvwxyz\n"
            "this is other row\n"; // u8 表示 UTF-8 编码
        fputs(str, fp);
        fclose(fp);

        fp = fopen("fopen_test.txt", "r");
        if (fp == NULL) {
            //printf("文件打开失败！%s\n", strerror(errno));
            perror("文件打开失败！"); // perror() 函数显示系统错误信息，效果同上，不返回值
            return errno;
        }

        // fgetc() 与 fgets() 函数
        char chs[100];
        fgets(chs, 100, fp); // 一行少于 99 个字符则读取一行，多于只读取前 99 个字符
        printf("%s", chs);
        char ch;
        while ((ch = fgetc(fp)) != EOF)
            printf("%c ", ch);
        printf("\n");

        // fseek()、ftell() 与 rewind() 函数
        fseek(fp, 15, SEEK_SET); // 移动到文件开始位置往后 15 的位置
        printf("当前位置：%ld\n", ftell(fp)); // 输出当前位置
        printf("读取到第 15 个字符：%c\n", fgetc(fp));
        fseek(fp, -5, SEEK_CUR); // 移动到当前位置往前 1 个字符的位置
        printf("当前位置：%ld\n", ftell(fp)); // 输出当前位置
        printf("在当前位置读取到字符：%c\n", fgetc(fp));
        rewind(fp); // 移动到文件开始位置
        printf("测试回到文件起始位置：%ld\n", ftell(fp)); // 输出当前位置

        // 关闭文件
        fclose(fp);
        fp = NULL; // 防止野指针

        print_end_line("读文件部分测试结束");
    }

    // 写文件部分，含部分读文件操作
    { // 打开文件
        print_start_line("写文件部分测试开始");
        fp = fopen("fopen_test_write.txt", "w");
        if (fp == NULL) {
            printf("文件打开失败！%s\n", strerror(errno));
            return errno;
        }

        // fputc() 与 fputs() 函数
        char str[] = "Hello, world!\n";
        fputs(str, fp);
        if (ferror(fp)) {
            printf("文件写入失败！%s\n", strerror(errno));
            return errno;
        }
        else
            printf("文件写入Hello, world!\\n成功！\n");
        char i = 'a';
        for (i = 'a'; i <= 'z'; ++i)
            if (fputc(i, fp) == EOF) {
                printf("文件写入失败！%s\n", strerror(errno));
                return errno;
            }
            else
                printf("写入%c ", i);
        fputc('\n', fp);
        printf("\n");

        // fprintf() 函数
        student s1 = { "Tom", 20, 88.5f };
        int wt1 = fprintf(fp, "姓名：%s，年龄：%d，成绩：%.1f\n", s1.name, s1.age, s1.score);
        if (wt1 == EOF) {
            perror("文件写入失败！");
            return errno;
        }
        fprintf(stdout, "写入数据为  姓名：%s，年龄：%d，成绩：%.1f\n",
            s1.name, s1.age, s1.score); // 输出到屏幕

        // fscanf() 函数
        int sc1 = fscanf(fp, ("姓名：%s，年龄：%d，成绩：%.1f\n",
            s1.name, &s1.age, (char*)&s1.score));
        if (sc1 == EOF) {
            perror("文件读取失败！");
            return errno;
        }
        printf("读取数据为  姓名：%s，年龄：%d，成绩：%.1f\n", s1.name, s1.age, s1.score);

        // 关闭文件
        fclose(fp);
        fp = NULL; // 防止野指针

        fp = fopen("fopen_write_binary.txt", "wb"); // 打开二进制文件
        if (fp == NULL) {
            printf("文件打开失败！%s\n", strerror(errno));
            return errno;
        }
        // fwrite() 函数
        char str2[] = "Hello, world!\n";
        size_t wt2 = fwrite(&s1, sizeof(student), 1, fp);
        if (wt2 == 0) {
            perror("文件写入失败！");
            return errno;
        }
        printf("写入二进制数据  姓名：%s，年龄：%d，成绩：%.1f\n", s1.name, s1.age, s1.score);
        // 关闭文件
        fclose(fp);
        fp = NULL; // 防止野指针

        // fread() 函数
        fp = fopen("fopen_write_binary.txt", "rb"); // 打开二进制文件
        student s2;

        size_t rd2 = fread(&s2, sizeof(student), 1, fp);
        if (rd2 == 0) {
            perror("文件读取失败！");
            return errno;
        }
        printf("读取二进制数据  姓名：%s，年龄：%d，成绩：%.1f\n", s2.name, s2.age, s2.score);
        // 关闭文件
        fclose(fp);
        fp = NULL; // 防止野指针

        // remove() 函数
        //int rm1 = remove("fopen_test_write.txt"); // 删除文件
        //printf("删除文件成功！%d\n", rm1);

        // rename() 函数
        //rm1 = rename("fopen_write_binary.txt", "fopen_write_binary_new.txt"); // 重命名文件
        //printf("重命名文件成功！%d\n", rm1);

        print_end_line("写文件部分测试结束");
    }

    // 补充函数部分
    {
        print_start_line("补充函数部分测试开始");
        student s3 = { "Jerry", 25, 90.0f };
        //sprintf() 函数： 更完整的版本为 snprintf() 函数
            /*int sprintf ( char * str, const char * format, ... );
                格式化输出到字符串，返回实际写入的字符数。使用在 printf 上使用 format 时打印
                的相同文本编写字符串，但内容不是打印，而是作为 C 字符串存储在 str 指向的缓冲区中。
                结尾自动添加一个空字符 null */
        char str3[50];
        sprintf(str3, "姓名：%s，年龄：%d，成绩：%.1f", s3.name, s3.age, s3.score);
        // str3 = "姓名：Jerry，年龄：25，成绩：90.0"
        printf("str3 = %s\n", str3);
        student s4 = { "", 0, 0.0f }; // 定义结构体变量
        //sscanf() 函数： 没有 snscanf() 函数
            /*int sscanf ( const char * str, const char * format, ... );
                从字符串中读取格式化数据，并将其解析到变量中。返回读取的元素个数。
                格式化字符串 format 与 scanf() 函数中的格式相同，但只解析字符串 str 中的内容，
                而不是从标准输入读取。 */
        char str4[] = "中二病也要起飞 22 66.6";
        int sc2 = sscanf(str4, "%s %d %f", // 不能是 %.1f
            s4.name, &s4.age, &s4.score);
        printf("s4 = 姓名：%s，年龄：%d，成绩：%.1f\n", s4.name, s4.age, s4.score);

        print_end_line("补充函数部分测试结束");
    }

    // 缓冲区理解
    {
        print_start_line("缓冲区理解测试开始");
        FILE* pf = fopen("test_flush.txt", "w");
        fputs("abcdef", pf);//先将代码放在输出缓冲区
        printf("睡眠10秒 - 已经写数据了, 打开 test_flush.txt 文件, 发现文件没有内容\n");
        Sleep(10000);
        printf("刷新缓冲区\n");
        fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到文件(磁盘)
        //注:fflush 在高版本的 vs 上不能使用了
        printf("再睡眠10秒 - 此时, 再次打开 test_flush.txt 文件, 文件有内容了\n");
        Sleep(10000);
        //fseek(pf, 0, SEEK_SET); // 移动到文件开始位置
        rewind(pf); // 移动到文件开始位置
        char ch = ' ';
        int i;
        for (i = 0; i < 6; ++i)
            (ch = fputc(ch, pf)) != EOF;
        printf("再次刷新缓冲区\n");
        fflush(pf);
        printf("此时再次打开 test_flush.txt 文件, 文件内容清空了\n");
        fclose(pf);//注:fclose在关闭文件的时候,也会刷新缓冲区
        pf = NULL;

        print_end_line("缓冲区理解测试结束");
    }
}

